package com.yfp.file.service;

import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.aliyun.oss.*;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.*;
import com.yfp.common.core.constant.DbConstants;
import com.yfp.common.core.domain.R;
import com.yfp.common.core.utils.file.FileUtils;
import com.yfp.common.core.utils.uuid.Snowflake;
import com.yfp.common.core.utils.uuid.UUID;
import com.yfp.file.config.AliyunOssConfig;
import com.yfp.file.domain.*;
import com.yfp.file.domain.*;
import com.yfp.file.mapper.PartUploadLogMapper;
import com.yfp.file.utils.FileUploadUtils;
import com.yfp.system.api.domain.SysFile;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;

/**
 * 阿里云-oss
 *
 * 
 * @date 2023/12/04
 */
@Service
public class OssSysFileServiceImpl implements ISysFileService {

    private static final Logger log = LoggerFactory.getLogger(OssSysFileServiceImpl.class);

    private static AliyunOssConfig aliyunOssConfig;

    @Resource
    private ImgResourceService imgResourceService;

    @Resource
    private VideoResourceService videoResourceService;

    @Resource
    private PartUploadLogMapper partUploadLogMapper;

    /**
     * 使用构造方法注入配置信息
     */
    @Autowired
    public OssSysFileServiceImpl(AliyunOssConfig aliyunOssConfig) {
        OssSysFileServiceImpl.aliyunOssConfig = aliyunOssConfig;
    }


    /**
     * 设置每个分片的大小(单位bytes)
     */
    private final static long partSize = 3 * 1024 * 1024;
    /**
     * 获取CPU核心线程数
     */
    private final static int cores = Runtime.getRuntime().availableProcessors();
    /**
     * 设置执行分片上传任务的线程池
     */
    public static ExecutorService threadPoolTaskExecutor = null;

    static {
        /**
         * 自定义一个线程池
         * 1.线程等待时间:
         *      根据初步测试 单片文件小于10M时,线程数充足的情况下,文件上传最短耗时、cpu时间累计耗时
         *          0.5M 747ms
         *          1M 1112ms
         *          2M 1881ms
         *          3M 4125ms
         *          5M 4213ms
         *          10M 4196ms
         * 2.核心线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间)* CPU数目
         * 3.线程等待时间较长,因此应保证足够的线程
         * 4.每一个分片的失败都会导致文件上传的失败,因此分片不宜过小
         *
         */
        threadPoolTaskExecutor = new ThreadPoolExecutor(cores * 4, // coreSize
                cores * 8, // maxSize
                60, // 60s
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(cores * 4),  // 有界队列
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    }


    @Override
    public String uploadFile(MultipartFile file) throws Exception {
        // 生成 OSSClient
        OSS ossClient = new OSSClientBuilder().build(aliyunOssConfig.getEndpoint(), aliyunOssConfig.getAccessKeyId(), aliyunOssConfig.getAccessKeySecret());
        ClientBuilderConfiguration conf = new ClientBuilderConfiguration();

        ObjectMetadata meta = new ObjectMetadata();
        // 设置MD5校验。
        String md5 = BinaryUtil.toBase64String(BinaryUtil.calculateMd5(file.getBytes()));
        meta.setContentMD5(md5);
        conf.setSupportCname(true);
        // 编码文件名
        String filePathName = FileUploadUtils.extractFilename(file);
        // 文件路径名称
        filePathName = aliyunOssConfig.getFilehost() + "/" + filePathName;

        if (filePathName.contains("apk")) {
            filePathName = filePathName + ".1";
        }
        try {
            ossClient.putObject(aliyunOssConfig.getBucketName(), filePathName, file.getInputStream());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return aliyunOssConfig.getUrl() + "/" + filePathName;
    }

    @Override
    public SysFile uploadFileByType(MultipartFile file, String type) throws Exception {
//        if (DbConstants.RESOURCE_TYPE_APK.equals(type)) {
//            return splittingAndUploadingFiles(file);
//        }
        SysFile sysFile = new SysFile();
        String url;
        long resultSize;
        try {
            // 上传并返回访问地址
            url = this.uploadFile(file);
            sysFile.setName(FileUtils.getName(url));
            sysFile.setUrl(url);

            resultSize = file.getSize();

        } catch (Exception e) {
            log.error("上传文件失败", e);
            throw new ServiceException("上传文件失败");
        }
        Snowflake snowflake = new Snowflake(1);
        switch (type) {
            case DbConstants.RESOURCE_TYPE_VIDEO:
                VideoResource videoResource = new VideoResource();
                videoResource.setVideoId(String.valueOf(snowflake.nextId()));
                videoResource.setUrl(url);
                videoResource.setSize(String.valueOf(resultSize));
                videoResource.setFormat(file.getContentType());
                videoResourceService.insert(videoResource);
                sysFile.setResourceId(videoResource.getVideoId());
                sysFile.setResourceType(DbConstants.RESOURCE_TYPE_VIDEO);
                break;
            case DbConstants.RESOURCE_TYPE_IMG:
                ImgResource imgResource = new ImgResource();
                imgResource.setImgId(String.valueOf(snowflake.nextId()));
                imgResource.setUrl(url);
                imgResource.setFormat(file.getContentType());
                imgResource.setSize(String.valueOf(resultSize));
                imgResourceService.insert(imgResource);
                sysFile.setResourceId(imgResource.getImgId());
                sysFile.setResourceType(DbConstants.RESOURCE_TYPE_IMG);
                break;
            case DbConstants.RESOURCE_TYPE_APK:
                sysFile.setResourceType(DbConstants.RESOURCE_TYPE_APK);
                sysFile.setSize(FileUploadUtils.convertToGB(resultSize));
                return sysFile;
            default:
                throw new ServiceException("未知类型，上传文件失败");
        }
        return sysFile;
    }


    public SysFile splittingAndUploadingFiles(MultipartFile file) throws Exception {

//        // 从环境变量中获取访问凭证。运行本代码示例之前，请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
//        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        String filePathName = FileUploadUtils.extractFilename(file);
        // 填写Object完整路径，例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = aliyunOssConfig.getFilehost() + "/" + filePathName;


        SysFile sysFile = new SysFile();
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(aliyunOssConfig.getEndpoint(), aliyunOssConfig.getAccessKeyId(), aliyunOssConfig.getAccessKeySecret());
        try {
            String uploadId = getUploadId(objectName, ossClient);

            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
            List<PartETag> partETags = new ArrayList<>();

            // 每个分片的大小，用于计算文件有多少个分片。单位为字节。
            final long partSize = 1 * 1024 * 1024L;   // 1 MB。

            // 根据上传的数据大小计算分片数。
            long fileLength = file.getSize();
            if (fileLength <= 0) {
                // 文件大小异常处理
                throw new ServiceException("文件异常");
            }
            int partCount = (int) (fileLength / partSize);
            if (fileLength % partSize != 0) {
                partCount++;
            }

            /**
             * 创建多线程上传的线程池和同步计数器
             */
            CountDownLatch partCountDownLatch = new CountDownLatch(partCount);
            // 遍历分片上传。
            InputStream partStream = null;


            // 遍历分片上传。
            for (int i = 0; i < partCount; i++) {
                long startPos = i * partSize;
                long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
                // 获取当前分片的数据流
                partStream = file.getInputStream();
                partStream.skip(startPos);
                // 创建UploadPartRequest对象
                UploadPartRequest uploadPartRequest = new UploadPartRequest(aliyunOssConfig.getBucketName(), objectName, uploadId, i + 1, partStream, curPartSize);

                threadPoolTaskExecutor.execute(new Runnable() {
                    /**
                     * When an object implementing interface {@code Runnable} is used
                     * to create a thread, starting the thread causes the object's
                     * {@code run} method to be called in that separately executing
                     * thread.
                     * <p>
                     * The general contract of the method {@code run} is that it may
                     * take any action whatsoever.
                     *
                     * @see Thread#run()
                     */
                    @Override
                    public void run() {
                        // 上传当前分片
                        UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                        // 将上传结果的PartETag添加到partETags中
                        partETags.add(uploadPartResult.getPartETag());

                        partCountDownLatch.countDown();
                    }
                });

            }
            /**
             * 同步计数器,保证所有分片上传成功后,才执行验证及后续操作
             */
            partCountDownLatch.await();

            // 创建CompleteMultipartUploadRequest对象。
            CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(aliyunOssConfig.getBucketName(), objectName, uploadId, partETags);

            // 如果需要在完成分片上传的同时设置文件访问权限，请参考以下示例代码。
            // completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.Private);

            // 完成分片上传。
            CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);

            log.info("分片上传完成,{}", completeMultipartUploadResult.getETag());

            String url = aliyunOssConfig.getUrl() + "/" + objectName;
            sysFile.setName(FileUtils.getName(url));
            sysFile.setUrl(url);
            sysFile.setSize(FileUploadUtils.convertToGB(file.getSize()));

        } catch (OSSException oe) {
            log.info("分片上传异常OSSException,{}", oe.getMessage());
        } catch (ClientException ce) {
            log.info("分片上传异常ClientException,{}", ce.getMessage());
            // 异常处理
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return sysFile;
    }

    /**
     * 获取上传全局唯一ID
     *
     * @return {@link String}
     */
    @Override
    public String getUploadId(String fileName) {
        if (CharSequenceUtil.isBlank(fileName)) {
            throw new ServiceException("文件名不能为空");
        }

        String objectName = getFilePathName(fileName);

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(aliyunOssConfig.getEndpoint(), aliyunOssConfig.getAccessKeyId(), aliyunOssConfig.getAccessKeySecret());
        try {
            return getUploadId(objectName, ossClient);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

    }

    /**
     * 分片上传
     *
     * @param param 参数
     * @return {@link R}
     */
    @Override
    public R partUpload(MultipartUploadDto param) {

        String uploadId = param.getUploadId();
        StopWatch st = new StopWatch();
        st.start();
        // 封装分片上传请求
        UploadPartRequest uploadPartRequest = new UploadPartRequest();
        uploadPartRequest.setUploadId(param.getUploadId());
        // part大小 1-10000
        uploadPartRequest.setPartNumber(param.getPartNumber());
        uploadPartRequest.setPartSize(param.getPartSize());
        uploadPartRequest.setBucketName(aliyunOssConfig.getBucketName());
        try {
            uploadPartRequest.setInputStream(param.getFile().getInputStream());
        } catch (IOException e) {
            throw new ServiceException("文件流异常");
        }

        PartUploadLog partUploadLog = partUploadLogMapper.queryById(uploadId);
        if (partUploadLog == null) {
            throw new ServiceException("上传标识符异常!");
        }
        String objectName = partUploadLog.getPaths();
        uploadPartRequest.setKey(objectName);
        // 查询该分片是否已上传
        boolean existPartNumber = partUploadLogMapper.countByUploadIdAndPartNumber(uploadId, uploadPartRequest.getPartNumber()) > 0;
        if (existPartNumber) {
            return R.ok();
        }

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(aliyunOssConfig.getEndpoint(), aliyunOssConfig.getAccessKeyId(), aliyunOssConfig.getAccessKeySecret());

        // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
        UploadPartResult uploadPartResult = null;
        try {
            uploadPartResult = ossClient.uploadPart(uploadPartRequest);
        } catch (OSSException e) {
            log.info("上传失败,{}", e.getMessage());
            throw new ServiceException("上传失败");
        } catch (com.aliyun.oss.ClientException e) {
            log.info("上传失败,客户端错误:{}", e.getMessage());
            throw new ServiceException("上传失败,客户端错误");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        st.stop();
        log.info("{}文件第 {} 片上传成功,上传结果:{},耗时:{}", uploadId, uploadPartRequest.getPartNumber(), JSON.toJSON(uploadPartResult), st.getTotalTimeMillis());
        // 保留分片上传接口
        savePartUploadRecord(uploadId, param, objectName, JSON.toJSON(uploadPartResult).toString());

        // 主动合并
//        List<PartETag> partTagList = uploadService.findPartTagListByUploadId(uploadId);
//
//        if (partTagList.size() == total) {
//            //上传成功
//            // 创建CompleteMultipartUploadRequest对象。
//            // 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。
//            // 当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件。
//            mergeMultipart(uploadId, objectName, partTagList);
//            uploadService.updateFileUpload(uploadId);
//        }


        return R.ok();

    }

    private void savePartUploadRecord(String uploadId, MultipartUploadDto param, String paths, String uploadResult) {
        PartUploadLog partUploadLog = new PartUploadLog();
        partUploadLog.setUploadId(uploadId);
        partUploadLog.setPartNumber(param.getPartNumber());
        partUploadLog.setMd5(param.getMd5());
        partUploadLog.setUploadResult(uploadResult);
        partUploadLog.setPaths(paths);
        partUploadLog.setSize(param.getPartSize());
        partUploadLogMapper.insert(partUploadLog);
    }


    /**
     * 获取文件路径名
     * 填写Object完整路径，例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
     *
     * @param fileName 文件名
     * @return {@link String}
     */
    @NotNull
    private static String getFilePathName(String fileName) {
        String suffixName = FileNameUtil.getSuffix(fileName);
        String filePathName = UUID.randomUUID().toString().replaceAll("-", "") + "." + suffixName;
        return aliyunOssConfig.getFilehost() + "/" + filePathName;
    }

    private String getUploadId(String objectName, OSS ossClient) {
        // 创建InitiateMultipartUploadRequest对象。
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(aliyunOssConfig.getBucketName(), objectName);

        // 初始化分片。
        InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);

        PartUploadLog partUploadLog = new PartUploadLog();
        partUploadLog.setUploadId(upresult.getUploadId());
        partUploadLog.setPaths(objectName);
        this.partUploadLogMapper.insert(partUploadLog);
        log.info("初始化分片上传成功,uploadId:{}", upresult.getUploadId());
        // 返回uploadId。
        return upresult.getUploadId();
    }


    private SysFile mergeMultipart(String uploadId, String objectName, List<PartETag> partTagList) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(aliyunOssConfig.getEndpoint(), aliyunOssConfig.getAccessKeyId(), aliyunOssConfig.getAccessKeySecret());
        try {
            StopWatch st = new StopWatch();
            st.start();
            CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(aliyunOssConfig.getBucketName(), objectName, uploadId, partTagList);
            log.info("{}文件上传完成,开始合并,partList:{}", uploadId, JSON.toJSON(partTagList));
            // 完成分片上传。

            CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
            st.stop();


            long totalSize = partTagList.stream().mapToLong(PartETag::getPartSize).sum(); // 计算总和
            log.info("{}文件上传完成,上传结果:{},大小:{},耗时:{}", uploadId, JSON.toJSON(completeMultipartUploadResult), totalSize, st.getTotalTimeMillis());

            SysFile sysFile = new SysFile();
            String url = aliyunOssConfig.getUrl() + "/" + objectName;
            sysFile.setName(FileUtils.getName(url));
            sysFile.setUrl(url);
            sysFile.setSize(FileUploadUtils.convertToGB(totalSize));

            return sysFile;
        } catch (OSSException e) {
            log.error("合并失败:{}", e.getMessage());
            throw new ServiceException("合并失败");
        } catch (com.aliyun.oss.ClientException e) {
            log.error("合并失败,客户端异常:{}", e.getMessage());
            throw new ServiceException("合并失败,客户端异常");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }


    public SysFile completeMultipartUpload(String uploadId) {
        PartUploadLog partUploadLog = partUploadLogMapper.queryById(uploadId);
        if (partUploadLog == null) {
            throw new ServiceException("上传标识符异常!");
        }

        List<PartETag> partTagList = partTagList(uploadId);

        return mergeMultipart(uploadId, partUploadLog.getPaths(), partTagList);
    }

    @Override
    public void validateFile(MultipartFile file, FileProperty fileProperty) {
        if (StrUtil.equals("1", fileProperty.getType())) {
            if (BooleanUtil.isTrue(fileProperty.getValidateAspectRatio3To2())) {
                Boolean aspectRatio3to2 = this.isAspectRatio3to2(file);
                Assert.isTrue(aspectRatio3to2, "图片宽高比不为3:2");
            } else if (BooleanUtil.isTrue(fileProperty.getValidateAspectRatio5To4())) {
                Boolean aspectRatio5To4 = this.isAspectRatio5To4(file);
                Assert.isTrue(aspectRatio5To4, "图片宽高比不为5:4");
            } else if(BooleanUtil.isTrue(fileProperty.getValidateRange())) {
                Boolean validateRange = this.isValidateRange(file, fileProperty);
                Assert.isTrue(validateRange, "图片宽高不满足");
            } else if (Objects.nonNull(fileProperty.getImagePx())) {
                Boolean validatePixel = this.validatePixel(file, fileProperty);
                String imagePx = fileProperty.getImagePx();
                List<Integer> imagePxList = JSONUtil.toList(imagePx, Integer.class);
                Assert.isTrue(validatePixel, "图片宽高不满足" + imagePxList.get(0) + "*" + imagePxList.get(1));
            }
        }
    }

    @Override
    public Boolean isAspectRatio3to2(MultipartFile file) {
        try (InputStream inputStream = file.getInputStream()) {
            // 读取图片
            BufferedImage image = ImageIO.read(inputStream);

            // 获取图片的宽度和高度
            int width = image.getWidth();
            int height = image.getHeight();

            // 计算宽高比
            double aspectRatio = (double) width / height;

            // 检查是否为3:2比例
            return Math.abs(aspectRatio - 1.5) < 0.01; // 允许一定的误差范围
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public Boolean isAspectRatio5To4(MultipartFile file) {
        try (InputStream inputStream = file.getInputStream()) {
            // 读取图片
            BufferedImage image = ImageIO.read(inputStream);

            // 获取图片的宽度和高度
            int width = image.getWidth();
            int height = image.getHeight();

            // 计算宽高比
            double aspectRatio = (double) width / height;

            // 检查是否为5:4比例
            return Math.abs(aspectRatio - 1.25) < 0.01; // 允许一定的误差范围
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public Boolean isValidateRange(MultipartFile file, FileProperty fileProperty) {
        try (InputStream inputStream = file.getInputStream()) {
            // 读取图片
            BufferedImage image = ImageIO.read(inputStream);

            // 获取图片的宽度和高度
            int width = image.getWidth();
            int height = image.getHeight();

            String imagePx = fileProperty.getImagePx();
            List<Integer> imagePxList = JSONUtil.toList(imagePx, Integer.class);
            if (width >= imagePxList.get(0) && width <= imagePxList.get(1)){
                return true;
            }
            if (height >= imagePxList.get(2) && height <= imagePxList.get(3)){
                return true;
            }
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public Boolean validatePixel(MultipartFile file, FileProperty fileProperty) {
        try (InputStream inputStream = file.getInputStream()) {
            // 读取图片
            BufferedImage image = ImageIO.read(inputStream);

            // 获取图片的宽度和高度
            int width = image.getWidth();
            int height = image.getHeight();

            String imagePx = fileProperty.getImagePx();
            List<Integer> imagePxList = JSONUtil.toList(imagePx, Integer.class);
            if (!NumberUtil.equals(imagePxList.get(0), width)) {
                return false;
            }
            if (!NumberUtil.equals(imagePxList.get(1), height)) {
                return false;
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    private List<PartETag> partTagList(String uploadId) {
        List<PartUploadLog> partUploadLogList = partUploadLogMapper.findPartTagListByUploadId(uploadId);
        return partUploadLogList.stream().map(item -> {
            String uploadResult = item.getUploadResult();
            return JSON.parseObject(uploadResult, PartETag.class);
        }).toList();
    }


}
